#lang racket

(require "util/utils.scm")
(provide scope-create scope-add-temporary scope-add-var! scope-list)

;; This is a utility object that helps manage scope to keep track of
;; free and bound variables.

;; it has gained the functionality of also tracking whether a variable
;; is bound by lambda or is just a temporary LET* binding.
;; this meant changing the list of variables into an assoc list of
;; (<var> <type>)

;; when creating a new scope to manage (lambda <bindings> ...)
;; you can use scope-create
;;
;; when you need to add temporaries to an existing scope, to manage LET*
;; you can use scope-add-temporary

(define (scope-create local-bindings other-scopes)
  (cons (cons (map make-local-var local-bindings) (box '())) other-scopes))

(define (make-local-var x) (cons x 'loc))
(define (make-tmp-var x) (cons x 'tmp))

(define (scope-add-temporary tmp scope)
  (if (null? scope)
      (cons (cons (list (make-tmp-var tmp)) (box '())) '())
      (let ((bindings (caar scope))
            (closed-over (cdar scope))
            (other-scopes (cdr scope)))
        (cons (cons (cons (make-tmp-var tmp) bindings) closed-over) other-scopes))))

;; to understand this function, consider:
;;
;; (lambda (x) (lambda (y) (lambda (z) (lambda (w) y))))
;;    ^ s1        ^ s2        ^ s3        ^ s4     ^ var
;;
;; if our scope object was (w/s4 z/s3 y/s2 x/s1) and we (add-var! scope 'y)
;; then it will add y to s4, then add it to s3 then it will stop because
;; y is bound here.
;;
;; it provides an error message if no scope binds the given variable
;; it returns #t if the variable is bound by the current scope
;;    otherwise the variable was bound by a later scope (and must be
;;    part of a closure)

(define (scope-add-var! scopes var) (scope-add-var!* scopes var #t))

(define (scope-add-var!* scopes var first)
  (if (null? scopes)
      (error 'scope-add-var!* "unbound variable" var)
      (let* ((local-bindings (caar scopes))
             (closed-over (cdar scopes)))
        (cond ((assoc var local-bindings)
               => (lambda (var-type)
                    (if first (cdr var-type) #f)))
              (else (set-box! closed-over (merge-cons var (unbox closed-over)))
                    (scope-add-var!* (cdr scopes) var #f))))))

(define (scope-list s)
  (unbox (cdar s)))
